Passed
Pull Request — master (#136)
by
unknown
01:54
created

index.ts ➔ writeAsync   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 13
c 0
b 0
f 0
rs 9.8
cc 3
1
import * as fs from 'fs'
2
import * as ID3Util from './src/ID3Util'
3
import * as ID3Helpers from './src/ID3Helpers'
4
import { isFunction, isString } from './src/util'
5
import { Tags, WriteTags } from './src/Tags'
6
import { Options } from './src/Options'
7
import { updateTags } from './src/update'
8
9
export { Tags, WriteTags } from "./src/Tags"
10
export { TagConstants } from './src/TagConstants'
11
12
// Used specification: http://id3.org/id3v2.3.0
13
14
type WriteCallback = {
15
    (error: null, data: Buffer): void
16
    (error: NodeJS.ErrnoException | Error, data: null): void
17
}
18
19
type ReadCallback = {
20
    (error: NodeJS.ErrnoException | Error, tags: null): void
21
    (error: null, tags: Tags): void
22
}
23
24
type RemoveCallback =
25
    (error: NodeJS.ErrnoException | Error | null) => void
26
27
type CreateCallback =
28
    (data: Buffer) => void
29
30
/**
31
 * Check and remove already written ID3-Frames from a buffer
32
 */
33
export function removeTagsFromBuffer(data: Buffer) {
34
    const framePosition = ID3Util.getFramePosition(data)
35
36
    if (framePosition === -1) {
37
        return data
38
    }
39
40
    const encodedSize = data.subarray(framePosition + 6, framePosition + 10)
41
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
42
        return false
43
    }
44
45
    if (data.length >= framePosition + 10) {
46
        const size = ID3Util.decodeSize(encodedSize)
47
        return Buffer.concat([
48
            data.subarray(0, framePosition),
49
            data.subarray(framePosition + size + 10)
50
        ])
51
    }
52
53
    return data
54
}
55
56
function writeInBuffer(tags: Buffer, buffer: Buffer) {
57
    buffer = removeTagsFromBuffer(buffer) || buffer
58
    return Buffer.concat([tags, buffer])
59
}
60
61
function writeAsync(tags: Buffer, filepath: string, callback: WriteCallback) {
62
    fs.readFile(filepath, (error, data) => {
63
        if(error) {
64
            callback(error, null)
65
            return
66
        }
67
        const newData = writeInBuffer(tags, data)
68
        fs.writeFile(filepath, newData, 'binary', (error) => {
69
            if (error) {
70
                callback(error, null)
71
            } else {
72
                callback(null, newData)
73
            }
74
        })
75
    })
76
}
77
78
function writeSync(tags: Buffer, filepath: string) {
79
    try {
80
        const data = fs.readFileSync(filepath)
81
        const newData = writeInBuffer(tags, data)
82
        fs.writeFileSync(filepath, newData, 'binary')
83
        return true
84
    } catch(error) {
85
        return error as Error
86
    }
87
}
88
89
/**
90
 * Write passed tags to a file/buffer
91
 */
92
export function write(tags: WriteTags, buffer: Buffer): Buffer
93
export function write(tags: WriteTags, filepath: string): true | Error
94
export function write(
95
    tags: WriteTags, filebuffer: string | Buffer, callback: WriteCallback
96
): void
97
export function write(
98
    tags: WriteTags,
99
    filebuffer: string | Buffer,
100
    callback?: WriteCallback
101
): Buffer | true | Error | void {
102
    const tagsBuffer = create(tags)
103
104
    if(isFunction(callback)) {
105
        if (isString(filebuffer)) {
106
            return writeAsync(tagsBuffer, filebuffer, callback)
107
        }
108
        return callback(null, writeInBuffer(tagsBuffer, filebuffer))
109
    }
110
    if(isString(filebuffer)) {
111
        return writeSync(tagsBuffer, filebuffer)
112
    }
113
    return writeInBuffer(tagsBuffer, filebuffer)
114
}
115
116
/**
117
 * Creates a buffer containing the ID3 Tag
118
 */
119
export function create(tags: WriteTags): Buffer
120
export function create(tags: WriteTags, callback: CreateCallback): void
121
export function create(tags: WriteTags, callback?: CreateCallback) {
122
    const frames = ID3Helpers.createBufferFromTags(tags)
123
124
    //  Create ID3 header
125
    const header = Buffer.alloc(10)
126
    header.fill(0)
127
    header.write("ID3", 0)              //File identifier
128
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
129
    header.writeUInt16BE(0x0000, 5)     //Flags 00
130
    ID3Util.encodeSize(frames.length).copy(header, 6)
131
132
    const id3Data = Buffer.concat([header, frames])
133
134
    if(isFunction(callback)) {
135
        return callback(id3Data)
136
    }
137
    return id3Data
138
}
139
140
function readSync(filebuffer: string | Buffer, options: Options): Tags {
141
    if(isString(filebuffer)) {
142
        filebuffer = fs.readFileSync(filebuffer)
143
    }
144
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
145
}
146
147
function readAsync(
148
    filebuffer: string | Buffer,
149
    options: Options,
150
    callback: ReadCallback
151
) {
152
    if(isString(filebuffer)) {
153
        fs.readFile(filebuffer, (error, data) => {
154
            if(error) {
155
                callback(error, null)
156
            } else {
157
                callback(null, ID3Helpers.getTagsFromBuffer(data, options))
158
            }
159
        })
160
    } else {
161
        callback(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
162
    }
163
}
164
165
/**
166
 * Read ID3-Tags from passed buffer/filepath
167
 */
168
export function read(filebuffer: string | Buffer, options?: Options): Tags
169
export function read(filebuffer: string | Buffer, callback: ReadCallback): void
170
export function read(
171
    filebuffer: string | Buffer, options: Options, callback: ReadCallback
172
): void
173
export function read(
174
    filebuffer: string | Buffer,
175
    optionsOrCallback?: Options | ReadCallback,
176
    callback?: ReadCallback
177
): Tags | void {
178
    const options: Options =
179
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
180
    callback =
181
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
182
183
    if(isFunction(callback)) {
184
        return readAsync(filebuffer, options, callback)
185
    }
186
    return readSync(filebuffer, options)
187
}
188
189
/**
190
 * Update ID3-Tags from passed buffer/filepath
191
 */
192
export function update(
193
    tags: WriteTags,
194
    buffer: Buffer,
195
    options?: Options
196
): Buffer
197
export function update(
198
    tags: WriteTags,
199
    filepath: string,
200
    options?: Options
201
): true | Error
202
export function update(
203
    tags: WriteTags,
204
    filebuffer: string | Buffer,
205
    callback: WriteCallback
206
): void
207
export function update(
208
    tags: WriteTags,
209
    filebuffer: string | Buffer,
210
    options: Options,
211
    callback: WriteCallback
212
): void
213
export function update(
214
    tags: WriteTags,
215
    filebuffer: string | Buffer,
216
    optionsOrCallback?: Options | WriteCallback,
217
    callback?: WriteCallback
218
): Buffer | true | Error | void {
219
    const options: Options =
220
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
221
    callback =
222
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
223
224
    const currentTags = read(filebuffer, options)
225
    const updatedTags = updateTags(tags, currentTags)
226
    if (isFunction(callback)) {
227
        return write(updatedTags, filebuffer, callback)
228
    }
229
    if (isString(filebuffer)) {
230
        return write(updatedTags, filebuffer)
231
    }
232
    return write(updatedTags, filebuffer)
233
}
234
235
function removeTagsSync(filepath: string) {
236
    let data
237
    try {
238
        data = fs.readFileSync(filepath)
239
    } catch(error) {
240
        return error as Error
241
    }
242
243
    const newData = removeTagsFromBuffer(data)
244
    if(!newData) {
245
        return false
246
    }
247
248
    try {
249
        fs.writeFileSync(filepath, newData, 'binary')
250
    } catch(error) {
251
        return error as Error
252
    }
253
254
    return true
255
}
256
257
function removeTagsAsync(filepath: string, callback: RemoveCallback) {
258
    fs.readFile(filepath, (error, data) => {
259
        if(error) {
260
            callback(error)
261
            return
262
        }
263
264
        const newData = removeTagsFromBuffer(data)
265
        if(!newData) {
266
            callback(error)
267
            return
268
        }
269
270
        fs.writeFile(filepath, newData, 'binary', (error) => {
271
            if(error) {
272
                callback(error)
273
            } else {
274
                callback(null)
275
            }
276
        })
277
    })
278
}
279
280
/**
281
 * Remove already written ID3-Frames from a file
282
 */
283
export function removeTags(filepath: string): boolean | Error
284
export function removeTags(filepath: string, callback: RemoveCallback): void
285
export function removeTags(filepath: string, callback?: RemoveCallback) {
286
    if(isFunction(callback)) {
287
        return removeTagsAsync(filepath, callback)
288
    }
289
    return removeTagsSync(filepath)
290
}
291
292
type Settle<T> = {
293
    (error: NodeJS.ErrnoException | Error, result: null): void
294
    (error: null, result: T): void
295
}
296
297
function makePromise<T>(callback: (settle: Settle<T>) => void) {
298
    return new Promise<T>((resolve, reject) => {
299
        callback((error, result) => {
300
            if(error) {
301
                reject(error)
302
            } else {
303
                // result can't be null here according the Settle callable
304
                // type but TS can't evaluate it properly here, so use the
305
                // null assertion, and then disable the lint error.
306
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307
                resolve(result!)
308
            }
309
        })
310
    })
311
}
312
313
export const Promises = {
314
    create: (tags: Tags) =>
315
        makePromise((settle: Settle<Buffer>) =>
316
            create(tags, result => settle(null, result)),
317
    ),
318
    write: (tags: Tags, filebuffer: string | Buffer) =>
319
        makePromise<Buffer>((callback: WriteCallback) =>
320
            write(tags, filebuffer, callback)
321
        ),
322
    update: (tags: Tags, filebuffer: string | Buffer, options?: Options) =>
323
        makePromise<Buffer>((callback: WriteCallback) =>
324
            update(tags, filebuffer, options ?? {}, callback)
325
        ),
326
    read: (file: string, options?: Options) =>
327
        makePromise((callback: ReadCallback) =>
328
            read(file, options ?? {}, callback)
329
        ),
330
    removeTags: (filepath: string) =>
331
        makePromise((settle: Settle<void>) =>
332
            removeTags(
333
                filepath,
334
                (error) => error ? settle(error, null) : settle(null)
335
            )
336
        )
337
} as const
338
339
/**
340
 * @deprecated consider using `Promises` instead, `Promise` creates conflict
341
 *             with the Javascript native promise.
342
 */
343
export { Promises as Promise }
344